# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# (C) 2012 Remek Zajac
#!/usr/bin/python
import xml.sax
import pyodbc
import cairo
import time
import sets
import math
import threading
import string

#local modules
import pyGeometry
import pyUtils


class Stats:
    def __init__(self):
        self.inserted = 0
        self.parsed = 0
        self.duplicated = 0

class RawNode(object):
    def __init__(self, id = None, latlon = None):
        self.nodeid = id
        self.latlon = latlon
        self.attributes = {}
        

class RawWay(object):
    def __init__(self, wayid):
        self.wayid = wayid
        self.attributes = {}

    def attrsFromString(self, s):
        for tuple in s.split(','):
            vk = tuple.split('=')
            assert(not self.attributes.has_key(vk[0]))
            self.attributes[vk[0]] = vk[1]
        
class RawWayNode(RawNode):
    def __init__(self, wayid, seq):
        super(RawWayNode, self).__init__()
        self.seq = seq
        self.wayid = wayid


class OSMGisDatabase(xml.sax.handler.ContentHandler):
    def __init__(self):
        print "connecting to OSMDBDataSource"
        self.conn = pyodbc.connect("DSN=OSMDBDataSource")
        print "finished connecting"
        self.m_cursor = self.conn.cursor()
        self.m_nodesStats = Stats()
        self.m_waynodeStats = Stats()
        self.m_wayStats = Stats()
        self.m_way = None
        self.m_seq = None
        self.m_parsingHighway = False
        self.m_waynodes = []
        self.m_wayInRegion = False
        self.m_timeStartSec = False

    def __del__(self):
        self.conn.close()

    def insertNode(self, node):
        sqlInsertCommand = ('insert into Nodes values ('+str(node.nodeid)+',POINT('+str(node.latlon[0])+','+str(node.latlon[1])+'),NULL)')
        try:
            self.m_cursor.execute(sqlInsertCommand)
            self.m_nodesStats.inserted+=1
        except pyodbc.IntegrityError:
            self.m_nodesStats.duplicated+=1

    def insertWay(self):
        assert(len(self.m_waynodes) > 0)
        assert(self.m_way)
        assert(len(self.m_way.attributes)>0)
        attrsStr = ('')
        for k,v in self.m_way.attributes.iteritems():
            if len(attrsStr) > 0:
                attrsStr += ','
            attrsStr += (k+'='+v)
        attrsStr = ('\'')+attrsStr+('\'')
        sqlInsertCommand = ('insert into Ways values ('+str(self.m_way.wayid)+','+attrsStr+')')
        try:
            self.m_cursor.execute(sqlInsertCommand)
            self.m_wayStats.inserted+=1
        except pyodbc.IntegrityError:
            self.m_wayStats.duplicated+=1        
        for waynode in self.m_waynodes:
            self.insertWayNode(waynode)

    def insertWayNode(self, waynode):
        sqlInsertCommand = ('insert into WayNodes values ('+str(waynode.wayid)+','+str(waynode.seq)+','+str(waynode.nodeid)+')')
        try:
            self.m_cursor.execute(sqlInsertCommand)
            self.m_waynodeStats.inserted+=1
        except pyodbc.IntegrityError:
                self.m_waynodeStats.duplicated+=1

    def insertNodeIfInRegion(self, node):
        if self.region.contains((node.latlon)):
            self.insertNode(node)

    def waynodeInRegion(self, waynode):
        sqlSelect = ('SELECT * FROM Nodes WHERE nodeid='+str(waynode.nodeid)+' AND latlon[0] BETWEEN '+str(min(self.region.tuple()[2], self.region.tuple()[0]))+' AND '+str(max(self.region.tuple()[2], self.region.tuple()[0]))+' AND latlon[1] BETWEEN '+str(min(self.region.tuple()[1], self.region.tuple()[3]))+' AND '+str(max(self.region.tuple()[1], self.region.tuple()[3])) )
        self.m_cursor.execute(sqlSelect)
        row = self.m_cursor.fetchone()
        if row:
            return True
        return False

    def printStats(self):
        currentTimeStr = time.strftime("%H:%M:%S")
        elapsedTimeStr = time.strftime("%H:%M:%S", time.gmtime(time.time()-self.m_timeStartSec))
        
        timeString = 'Still alive at:['+currentTimeStr+'] Elapsed: ['+elapsedTimeStr+']'
        nodeStatString = 'Nodes:('+str(self.m_nodesStats.parsed)+','+str(self.m_nodesStats.inserted)+','+str(self.m_nodesStats.duplicated)+'),'
        wayNodeStatString = 'WayNodes:('+str(self.m_waynodeStats.parsed)+','+str(self.m_waynodeStats.inserted)+','+str(self.m_waynodeStats.duplicated)+'),'
        wayStatString = 'Ways:('+str(self.m_wayStats.parsed)+','+str(self.m_wayStats.inserted)+','+str(self.m_wayStats.duplicated)+')'
        statString = nodeStatString+wayNodeStatString+wayStatString
        print timeString
        print statString

    def parseOsm(self, fromOsm, region):
        parser = xml.sax.make_parser()
        parser.setContentHandler(self)
        self.region = region
        print "**********************************************************************"
        print "Importing OSM data for region:", region.tuple()
        print "From file:", fromOsm
        print "Following stats are: (parsed, inserted, duplicated):"
        print "This may take between minutes and hours depending on the size of the input file"
        self.m_timeStartSec= time.time()
        parser.parse(fromOsm)
        self.printStats()
        self.conn.commit() #final commit
        print "Finished importing OSM data for region:", region.tuple()
        print "**********************************************************************"
        

    def startElement(self, name, attrs):
        if name == "node":
            node = RawNode()
            node.latlon = (float(attrs.getValue('lat')), float(attrs.getValue('lon')))
            node.nodeid = int(attrs.getValue('id'))
            self.insertNodeIfInRegion(node)
            self.m_nodesStats.parsed+=1
            if (self.m_nodesStats.parsed % 100000 == 0):
                self.printStats()
                self.conn.commit() #partial commit
        elif name == "way":
            self.m_way = RawWay(int(attrs.getValue('id')))
            self.m_seq = 0
            self.m_wayStats.parsed+=1
            if (self.m_wayStats.parsed % 10000 == 0):
                self.printStats()
                self.conn.commit() #partial commit
        elif name == "nd":
            waynode = RawWayNode(self.m_way.wayid, self.m_seq)
            waynode.nodeid = int(attrs.getValue('ref'))
            self.m_waynodeStats.parsed+=1                                
            if not self.m_wayInRegion:
                self.m_wayInRegion = self.waynodeInRegion(waynode)
            self.m_waynodes.append(waynode)
            self.m_seq+=1
        elif name == "tag":
            if self.m_way and attrs.has_key("k") and "highway" == attrs.getValue("k"):
                self.m_way.attributes["highway"] = attrs.getValue("v")
                
    def endElement(self, name):
        if name == "way":
            assert(self.m_way)
            if self.m_wayInRegion and self.m_way.attributes.has_key("highway"):
                self.insertWay()
            self.m_way = None
            self.m_seq = None
            self.m_waynodes = []
            self.m_wayInRegion = False

    def characters(self, chrs):
        pass
    def startDocument(self):
        pass 
    def endDocument(self):
        pass


class OSMGisSQLNodeCursor:
    EUnitialised = 0
    EWayNodes = 2

    threadLock = threading.Lock()

    @staticmethod
    def inRegionSqlCondition(region):
        if region:
            return ('Nodes.latlon[0] BETWEEN '+str(min(region.tuple()[2], region.tuple()[0]))+' AND '+str(max(region.tuple()[2], region.tuple()[0]))+' AND Nodes.latlon[1] BETWEEN '+str(min(region.tuple()[1], region.tuple()[3]))+' AND '+str(max(region.tuple()[1], region.tuple()[3])))
        else:
            return ("1=1")

    def __init__(self, db):
        self.threadLock.acquire() #database is sensitive for number of connections
        self.db = db
        self.m_cursor = db.conn.cursor()
        self.op = OSMGisSQLNodeCursor.EUnitialised
        self.way = None
        
    def __del__(self):
        self.threadLock.release()

    def wayNodesNearestTo(self, geometrysqlstatement):
        sql = ('SELECT * FROM WayNodes JOIN Nodes ON WayNodes.nodeid=Nodes.nodeid JOIN Ways on WayNodes.wayid = Ways.wayid ORDER BY Nodes.latlon <-> '+geometrysqlstatement+'LIMIT 1')
        self.m_cursor.execute(sql)
        self.op = OSMGisSQLNodeCursor.EWayNodes

    def wayNodesForRegion(self, region):
        nestedsql = ('SELECT DISTINCT WayNodes.wayid FROM WayNodes JOIN Nodes ON WayNodes.nodeid=Nodes.nodeid WHERE '+self.inRegionSqlCondition(region))
        self.wayNodes(nestedsql)

    def wayNodesForNodeId(self, nodeid):
        nestedsql = ('SELECT DISTINCT WayNodes.wayid FROM WayNodes JOIN Nodes ON WayNodes.nodeid=Nodes.nodeid WHERE WayNodes.nodeid='+str(nodeid))
        self.wayNodes(nestedsql)
        
    def wayNodes(self, nestedsql):
        sql = ('SELECT * FROM WayNodes JOIN Nodes ON WayNodes.nodeid=Nodes.nodeid JOIN Ways on WayNodes.wayid = Ways.wayid WHERE WayNodes.wayid IN ('+nestedsql+') ORDER BY WayNodes.wayid, WayNodes.seq')            
        self.m_cursor.execute(sql)
        self.op = OSMGisSQLNodeCursor.EWayNodes
                
    def fetchone(self):
        row = self.m_cursor.fetchone()
        if not row:
            raise EOFError()
        elif self.op == OSMGisSQLNodeCursor.EWayNodes:
            waynode = RawWayNode(row.wayid, row.seq)
            row.latlon = string.strip(row.latlon, ')(')
            lat,lon = string.split(row.latlon, ',')
            waynode.latlon = (float(lat), float(lon))
            waynode.nodeid = row.nodeid
            if not self.way or self.way.wayid != row.wayid:
                self.way = RawWay(row.wayid)
                self.way.attrsFromString(row.attrs)
            waynode.attributes = self.way.attributes
            return waynode
        else:
            raise AttributeError()


class WayNode:
    def __init__(self, parent, id, latlon):
        self.m_latlon = latlon
        self.m_id = id
        self.m_next = sets.Set() #navigable to waynodes
        self.m_prev = sets.Set() #navigable from waynodes
        self.m_evaluated = False #if this node (and all its 'next' references) can be trusted

    def id(self):
        return self.m_id

    def latlon(self):
        return self.m_latlon

    def setNext(self, waynode, oneway = False):
        if waynode.id() != self.id():
            self.m_next.add(waynode)
            waynode.m_prev.add(self)
            if not oneway:
                self.m_prev.add(waynode)
                waynode.m_next.add(self)

    def _evaluate(self):
        assert(not self.m_evaluated)
        WayNodes.Instance().expandJunction(self.m_id)
        self.m_evaluated = True

    def next(self):
        if not self.m_evaluated:
            self._evaluate()
        return self.m_next

class WayNodesRamCache:
    def __init__(self):
        self.waynodes = {} #nodeid hashed waynodes
        self.region = pyGeometry.GeoRegion()

    def append(self, waynodedb):
        waynode = None
        if self.waynodes.has_key(waynodedb.nodeid):
            waynode = self.waynodes[waynodedb.nodeid]
        else:
            waynode = WayNode(self, waynodedb.nodeid, waynodedb.latlon)
            self.waynodes[waynode.id()] = waynode
            self.region.mergeInto(waynode.latlon())
        return waynode

@pyUtils.Singleton
class WayNodes:
    ECacheSize = 1000 #nodes      

    def __init__(self):
        self.waynodesRAMCache = WayNodesRamCache()
        self.db = OSMGisDatabase()

    def getNearest(self, latlon):
        dbcursor = self.cursor()
        dbcursor.wayNodesNearestTo('POINT'+str(latlon))
        waynode = None
        try:
            waynodedb = dbcursor.fetchone()
            waynode = self.waynodesRAMCache.append(waynodedb)
        except EOFError:
            pass            
        return waynode

    def cursor(self):
        dbCursor = OSMGisSQLNodeCursor(self.db)
        return dbCursor

    def fetchNodes(self, cursor):
        previousWayNodeDb = None
        previousWayNode = None
        try:
            while 1:
                waynodedb = cursor.fetchone()
                waynode = self.waynodesRAMCache.append(waynodedb)
                if previousWayNodeDb and previousWayNodeDb.wayid == waynodedb.wayid:
                    assert(previousWayNodeDb.seq == waynodedb.seq-1)
                    assert(previousWayNode)
                    previousWayNode.setNext(waynode)
                previousWayNode = waynode
                previousWayNodeDb = waynodedb
        except EOFError:
            pass
        
    def expandJunction(self, nodeId):
        cursor = self.cursor()
        cursor.wayNodesForNodeId(nodeId)
        self.fetchNodes(cursor)
        



class WayNodesRAMCacheRenderer:
    ERepaintEveryGrowBy = 50
    
    def __init__(self, parent):
        self.parent = parent
        self.surface = None
        self.lastDrawingPassFor = 0 #cached nodes
        self.repaint = False
        self.cache = WayNodes.Instance()
        self.lastZoom = None

    def latlon2pix(self, latlon):
        x = int((latlon[1]-self.cache.waynodesRAMCache.region.nw()[1])*self.xfactor)
        y = int((self.cache.waynodesRAMCache.region.nw()[0]-latlon[0])*self.yfactor)
        return (x,y,True)                

    def draw(self, cr):
        if self.repaint:
            assert(self.surface)
            localcr = cairo.Context(self.surface)
            nodeDrawingTool = self.parent.drawingToolkit().nodeTool()
            nodeDrawingTool.init(localcr)
            cursor = WayNodes.Instance().cursor()   #grab the database cursor for the duration of the
                                                    #iteration. Bit of a hack, but prevents the
                                                    #dictionary from updates
            for waynode in self.cache.waynodesRAMCache.waynodes.itervalues():
                if len(waynode.m_next) > 2: #paint junctions only and don't expact (don't call next(),
                                            #as it raises dictionary changed during iteration
                    nodeDrawingTool.draw(self.latlon2pix(waynode.latlon()))
            self.lastZoom = self.parent.zoom
            self.lastDrawingPassFor = len(self.cache.waynodesRAMCache.waynodes)
            self.repaint = False
        
        if self.surface:             
            pix = self.parent.latlon2pix(self.cache.waynodesRAMCache.region.nw())
            cr.set_source_surface(self.surface,pix[0], pix[1])
            cr.paint()

    def invalidate(self, calledBy):
        cursor = WayNodes.Instance().cursor()   #grab the database cursor for the duration of the checks, prevents the RAM cache from changing from other threads     
        if not self.surface or self.lastDrawingPassFor+self.ERepaintEveryGrowBy < len(self.cache.waynodesRAMCache.waynodes) or self.lastZoom != self.parent.zoom:
            georegion = self.cache.waynodesRAMCache.region
            if not georegion.isNull():
                nw = self.parent.latlon2pix(georegion.nw())
                se = self.parent.latlon2pix(georegion.se())
                pixwidth = int(math.fabs(nw[0]-se[0]))
                pixheight = int(math.fabs(nw[1]-se[1]))
                self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, pixwidth, pixheight)
                self.repaint = True
                self.yfactor = 1.0* pixheight / georegion.height()
                self.xfactor = 1.0* pixwidth / georegion.width()
        else:
            self.repaint = False
         


    
